#Copyright 2008 Adam A. Crossland
#
#Licensed under the Apache License, Version 2.0 (the "License");
#you may not use this file except in compliance with the License.
#You may obtain a copy of the License at
#
#http://www.apache.org/licenses/LICENSE-2.0
#
#Unless required by applicable law or agreed to in writing, software
#distributed under the License is distributed on an "AS IS" BASIS,
#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#See the License for the specific language governing permissions and
#limitations under the License.

from google.appengine.ext import db

class SlugExistsException(Exception):
    """This exception indicates that it was impossible to efficiently generate
    a unique slug for the entity.  You should consider choosing to base the
    slug on a field that has more differentiation from other of its type."""

    pass

class SlugRoot(db.Model):
    """Instances of the SlugRoot class acts as root enities for instances of the
    Slug class.  Each Model class that has associated Slugs will have a matching
    SlugRoot which will be the parent of those entities.

    Keeping the slugs for entities of a given type in their own entity group
    allows for faster checking for duplicate slugs.

    This class will not be manipluated directly by code that is using Sluggable."""

    slug_type = db.StringProperty(required=True)

    @staticmethod
    def get_root(slug_class):
        "Get the root entity for slugs belonging to the class given in slug_class."
        slug_root_key_name = "slug_root_" + slug_class.__name__
        root = SlugRoot.get_by_key_name(slug_root_key_name)
        if root is None:
            root = SlugRoot(slug_type=slug_class.__name__, key_name=slug_root_key_name)
            root.put()
        return root

class Slug(db.Model):
    """The Slug class contains both the value of a given slug as well as a
    reference to the entity with which the slug is associated.

    Normally, this class will not be directly used by the developer, as all
    operations and behaviors are encapsulated in Sluggable."""

    slug_text = db.StringProperty(required=True)
    "The text by which the slug can be referred."
    slugged_entity = db.ReferenceProperty()
    "The datastore entity to which the slug refers."

    def __repr__(self):
        return self.slug_text

    def __str__(self):
        return self.slug_text

    @staticmethod
    def __slug_key(slug):
        return "slug_" + slug

    @staticmethod
    def add_slug(to_entity, slug_value):
        "Add a slug to the given datastore entity."
        root = SlugRoot.get_root(to_entity.__class__)
        existing_slug = Slug.get_by_key_name(Slug.__slug_key(slug_value), parent=root)
        if existing_slug is not None:
            raise SlugExistsException
        new_slug = Slug(parent=root, key_name=Slug.__slug_key(slug_value), slug_text=slug_value, slugged_entity=to_entity)
        new_slug.put()

        return new_slug

    @staticmethod
    def find_slugged(for_class, slug):
        "Find the entity that is associated with the given slug."
        root = SlugRoot.get_root(for_class)
        found_slugged_entity = None
        found_slug = Slug.get_by_key_name(Slug.__slug_key(slug), parent=root)
        if found_slug is not None:
            found_slugged_entity = found_slug.slugged_entity

        return found_slugged_entity

class Sluggable(db.Model):
    """This class is inherited by another db.Model-based class.  It provides
    the ability to associate with the given entity a slug: a short, descriptive
    piece of URL-friendly text."""

    slug = db.ReferenceProperty(Slug, collection_name="slugged")
    """The Slug entity that is associated with this entity.  Will be None if
    set_slug() has not been called."""

    @classmethod
    def slug_field_is(cls, slug_field):
        """The class that is mixing in Sluggable must call this method to
        define the model field who's value will be used to generate the slug."""

        if db.Property in slug_field.__class__.__bases__:
            cls.__slug_field = slug_field
        else:
            raise Exception, "Slug field must be a db.Property"

    def __get_slug_value(self):
        found = None
        try:
            found = self.slug.slug_text
        except:
            # Nothing to do here, we are jsut trying to prevent an error
            # being thrown.  Returning None indicates that this entity has
            # not gone through slug assignment
            pass
        return found

    slugged_as = property(__get_slug_value, None, None, "The slug assigned to this entity.")
    "The string value of the slug that is associated with thsi entity."

    def __get_slug_field_value(self):
        return str(self.__slug_field)

    def __make_slug(self, append=""):

        # It is possible to change the format of the slugs by modifying this
        # method.
        import re
        alphaspaces = re.compile(r"[^\w\s]")
        spaces = re.compile(r"\s")
        slug = spaces.sub("-", alphaspaces.sub(r"", self.__get_slug_field_value().lower()))

        return slug + append

    def set_slug(self):
        """Assign a slug to this entity.
        The entity will be saved after the slug has been created and saved.

        This method can not be called inside a transaction."""

        slug_try = self.__make_slug()
        slug_iter = 0
        slug_good = False
        while not slug_good and slug_iter <= 10:
            # There will only be ten attempts to make the slug unique by
            # appending digits to it.  More than that probably indicates that
            # field specified in the call to slug_field_is does not have enough
            # differentiation.
            try:
                self.slug = Slug.add_slug(self, self.__make_slug("_" + str(slug_iter) if slug_iter > 0 else ""))
                slug_good = True
                self.put()
            except SlugExistsException:
                slug_iter += 1
        if slug_iter == 11:
            # We have exceeded the maximum number of allowable attempts to de-dupe the slug, so throw
            # an exception.
            raise SlugExistsException, "Could not create a unique slug value."

    def delete_slug(self):
        """Remove the association of this entity with its slug.  The slug is
        deleted from the datastore."""
        self.slug.delete()
        self.slug = None
        self.put()

    @classmethod
    def find_by_slug(cls, slug):
        "Find entity of this type that is associated with the given slug."
        return Slug.find_slugged(cls, slug)
